package com.baidu.iit.pxp.delegate;

import com.baidu.iit.pvm.ActivityException;
import com.baidu.iit.pvm.ActivityIllegalArgumentException;
import com.baidu.iit.pvm.delegate.*;
import com.baidu.iit.pxp.BpmnError;
import com.baidu.iit.pxp.behavior.AbstractBpmnActivityBehavior;
import com.baidu.iit.pxp.behavior.ServiceTaskJavaDelegateActivityBehavior;
import com.baidu.iit.pxp.invocation.ExecutionListenerInvocation;
import com.baidu.iit.pxp.invocation.TaskListenerInvocation;
import com.baidu.iit.pxp.listener.TaskListener;
import com.baidu.iit.pxp.model.DelegateTask;
import com.baidu.iit.pxp.model.FieldDeclaration;
import com.baidu.iit.pxp.util.ErrorPropagation;
import com.baidu.iit.pxp.util.ReflectUtil;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;

/**
 * User: huangweili
 * Date: 14-4-30
 * Time: 下午3:35
 */
public class ClassDelegate extends AbstractBpmnActivityBehavior implements TaskListener, ExecutionListener {

    protected String className;
    protected List<FieldDeclaration> fieldDeclarations;
    protected ExecutionListener executionListenerInstance;
    protected TaskListener taskListenerInstance;
    protected ActivityBehavior activityBehaviorInstance;

    public ClassDelegate(String className, List<FieldDeclaration> fieldDeclarations) {
        this.className = className;
        this.fieldDeclarations = fieldDeclarations;
    }

    public ClassDelegate(Class<?> clazz, List<FieldDeclaration> fieldDeclarations) {
        this(clazz.getName(), fieldDeclarations);
    }

    public void notify(DelegateExecution execution) throws Exception {
        if (executionListenerInstance == null) {
            executionListenerInstance = getExecutionListenerInstance();
        }
        ExecutionListenerInvocation invocation=new ExecutionListenerInvocation(executionListenerInstance, execution);
        invocation.proceed();
    }

    protected ExecutionListener getExecutionListenerInstance() {
        Object delegateInstance = instantiateDelegate(className, fieldDeclarations);
        if (delegateInstance instanceof ExecutionListener) {
            return (ExecutionListener) delegateInstance;
        } else if (delegateInstance instanceof JavaDelegate) {
            return new ServiceTaskJavaDelegateActivityBehavior((JavaDelegate) delegateInstance);
        } else {
            throw new ActivityIllegalArgumentException(delegateInstance.getClass().getName()+" doesn't implement "+ExecutionListener.class+" nor "+JavaDelegate.class);
        }
    }

    public void notify(DelegateTask delegateTask) {
        if (taskListenerInstance == null) {
            taskListenerInstance = getTaskListenerInstance();
        }
        try {
            TaskListenerInvocation invocation= new TaskListenerInvocation(taskListenerInstance, delegateTask);
            invocation.proceed();
        }catch (Exception e) {
            throw new ActivityException("Exception while invoking TaskListener: "+e.getMessage(), e);
        }
    }

    protected TaskListener getTaskListenerInstance() {
        Object delegateInstance = instantiateDelegate(className, fieldDeclarations);
        if (delegateInstance instanceof TaskListener) {
            return (TaskListener) delegateInstance;
        } else {
            throw new ActivityIllegalArgumentException(delegateInstance.getClass().getName()+" doesn't implement "+TaskListener.class);
        }
    }

    // Activity Behavior
    public void execute(ActivityExecution execution) throws Exception {
        if (activityBehaviorInstance == null) {
            activityBehaviorInstance = getActivityBehaviorInstance(execution);
        }
        try {
            activityBehaviorInstance.execute(execution);
        } catch (BpmnError error) {
            ErrorPropagation.propagateError(error, execution);
        }
    }

    // Signallable activity behavior
    public void signal(ActivityExecution execution, String signalName, Object signalData) throws Exception {
        if (activityBehaviorInstance == null) {
            activityBehaviorInstance = getActivityBehaviorInstance(execution);
        }

        if (activityBehaviorInstance instanceof SignallableActivityBehavior) {
            ((SignallableActivityBehavior) activityBehaviorInstance).signal(execution, signalName, signalData);
        } else {
            throw new ActivityException("signal() can only be called on a " + SignallableActivityBehavior.class.getName() + " instance");
        }
    }

    protected ActivityBehavior getActivityBehaviorInstance(ActivityExecution execution) {
        Object delegateInstance = instantiateDelegate(className, fieldDeclarations);

        if (delegateInstance instanceof ActivityBehavior) {
            return determineBehaviour((ActivityBehavior) delegateInstance, execution);
        } else if (delegateInstance instanceof JavaDelegate) {
            return determineBehaviour(new ServiceTaskJavaDelegateActivityBehavior((JavaDelegate) delegateInstance), execution);
        } else {
            throw new ActivityIllegalArgumentException(delegateInstance.getClass().getName()+" doesn't implement "+JavaDelegate.class.getName()+" nor "+ActivityBehavior.class.getName());
        }
    }

    protected ActivityBehavior determineBehaviour(ActivityBehavior delegateInstance, ActivityExecution execution) {
        if (hasMultiInstanceCharacteristics()) {
            multiInstanceActivityBehavior.setInnerActivityBehavior((AbstractBpmnActivityBehavior) delegateInstance);
            return multiInstanceActivityBehavior;
        }
        return delegateInstance;
    }


    public static Object instantiateDelegate(Class<?> clazz, List<FieldDeclaration> fieldDeclarations) {
        return instantiateDelegate(clazz.getName(), fieldDeclarations);
    }

    public static Object instantiateDelegate(String className, List<FieldDeclaration> fieldDeclarations) {
        Object object = ReflectUtil.instantiate(className);
        applyFieldDeclaration(fieldDeclarations, object);
        return object;
    }

    public static void applyFieldDeclaration(List<FieldDeclaration> fieldDeclarations, Object target) {
        if(fieldDeclarations != null) {
            for(FieldDeclaration declaration : fieldDeclarations) {
                applyFieldDeclaration(declaration, target);
            }
        }
    }

    public static void applyFieldDeclaration(FieldDeclaration declaration, Object target) {
        Method setterMethod = ReflectUtil.getSetter(declaration.getName(),
                target.getClass(), declaration.getValue().getClass());

        if(setterMethod != null) {
            try {
                setterMethod.invoke(target, declaration.getValue());
            } catch (IllegalArgumentException e) {
                throw new ActivityException("Error while invoking '" + declaration.getName() + "' on class " + target.getClass().getName(), e);
            } catch (IllegalAccessException e) {
                throw new ActivityException("Illegal acces when calling '" + declaration.getName() + "' on class " + target.getClass().getName(), e);
            } catch (InvocationTargetException e) {
                throw new ActivityException("Exception while invoking '" + declaration.getName() + "' on class " + target.getClass().getName(), e);
            }
        } else {
            Field field = ReflectUtil.getField(declaration.getName(), target);
            if(field == null) {
                throw new ActivityIllegalArgumentException("Field definition uses unexisting field '" + declaration.getName() + "' on class " + target.getClass().getName());
            }
            if(!fieldTypeCompatible(declaration, field)) {
                throw new ActivityIllegalArgumentException("Incompatible type set on field declaration '" + declaration.getName()
                        + "' for class " + target.getClass().getName()
                        + ". Declared value has type " + declaration.getValue().getClass().getName()
                        + ", while expecting " + field.getType().getName());
            }
            ReflectUtil.setField(field, target, declaration.getValue());
        }
    }

    public static boolean fieldTypeCompatible(FieldDeclaration declaration, Field field) {
        if(declaration.getValue() != null) {
            return field.getType().isAssignableFrom(declaration.getValue().getClass());
        } else {
            // Null can be set any field type
            return true;
        }
    }


    public String getClassName() {
        return className;
    }

}
